Skip to content

Create resource_extra_fields_export.py#47

Open
ELN-uzun wants to merge 2 commits intoelabftw:masterfrom
ELN-uzun:patch-3
Open

Create resource_extra_fields_export.py#47
ELN-uzun wants to merge 2 commits intoelabftw:masterfrom
ELN-uzun:patch-3

Conversation

@ELN-uzun
Copy link
Contributor

@ELN-uzun ELN-uzun commented Mar 3, 2026

Python script to export extra fields of items in a specific resource category.

Summary by CodeRabbit

  • New Features
    • Added an example script to export items from a selected eLabFTW category to CSV and XLSX.
    • Exports use UTF-8 CSV output, deterministic column ordering, and normalized field names.
    • List-type values are rendered as comma-separated strings.
    • Generated XLSX includes bold headers, auto-filtering, and auto-sized columns.
    • Script prints completion messages when exports finish.

Python script to export extra fields of items in a specific resource category
@coderabbitai
Copy link

coderabbitai bot commented Mar 3, 2026

Walkthrough

A new example script was added to export items from an eLabFTW category to CSV and XLSX. It configures an API client, fetches items, normalizes extra_fields according to a fixed FIELD_ORDER, and writes UTF-8 CSV and formatted Excel outputs.

Changes

Cohort / File(s) Summary
New Export Example Script
examples/resource_extra_fields_export.py
Adds a standalone export utility that: configures elabapi_python client, retrieves all items for a category, normalizes and orders extra_fields, writes a UTF-8 CSV and an XLSX workbook with bold headers, auto-filter, and auto-sized columns.

Sequence Diagram(s)

sequenceDiagram
    participant Script as "Export Script"
    participant ApiClient as "elabapi_python.ApiClient"
    participant eLabAPI as "eLabFTW API"
    participant FS as "Filesystem (CSV/XLSX)"

    Script->>ApiClient: configure (API_HOST_URL, API_KEY)
    Script->>ApiClient: request items for CATEGORY_ID
    ApiClient->>eLabAPI: GET /items?category=CATEGORY_ID
    eLabAPI-->>ApiClient: return items JSON
    ApiClient-->>Script: deliver items
    Script->>Script: parse items, normalize extra_fields, apply FIELD_ORDER
    Script->>FS: write UTF-8 CSV (headers + rows)
    Script->>FS: create XLSX, write headers (bold), rows, autofilter, autosize
    Script-->>FS: close files
Loading

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

Make it so: data sails from API to shore,
Rows collide and extra fields align,
CSV hums, XLSX gleams once more,
Columns stretch, headers bold and fine,
📁✨ Export complete — engage, and archive.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Create resource_extra_fields_export.py' directly and specifically describes the primary change—a new Python script file being added to the examples directory.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (2)
examples/resource_extra_fields_export.py (2)

121-131: Recommend wrapping execution in a main guard, Ensign.

Running script logic at module level prevents reuse and testability. The Federation standard is to use if __name__ == "__main__": to encapsulate execution logic.

♻️ Proposed refactor: Add main guard
+def main():
+    print("Starting export...")
+
+    # Retrieve all items from the specified category
+    try:
+        items = itemsApi.read_items(cat=CATEGORY_ID)
+    except Exception as e:
+        print(f"Failed to retrieve items: {e}")
+        return
+
+    print(f"Items found: {len(items)}")
+    # ... rest of logic ...
+
+if __name__ == "__main__":
+    main()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/resource_extra_fields_export.py` around lines 121 - 131, The script
runs top-level logic (prints and calls itemsApi.read_items with CATEGORY_ID)
which prevents reuse and testing; move the execution into a main function (e.g.,
def main(): containing the print("Starting export..."), items =
itemsApi.read_items(cat=CATEGORY_ID), and print(f"Items found: {len(items)}"))
and then add the standard guard if __name__ == "__main__": main() so the module
can be imported without side effects.

173-182: A minor course correction for modern Python idioms.

The static analysis correctly notes that spread syntax is more elegant than concatenation. A small improvement, but precision matters on the bridge.

✨ Proposed refactor: Use spread operator
 # Define final column structure
-csv_columns = ["Ressourcen ID", "Titel"] + FIELD_ORDER
+csv_columns = ["Ressourcen ID", "Titel", *FIELD_ORDER]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/resource_extra_fields_export.py` around lines 173 - 182, Replace the
list concatenation used to build csv_columns with Python's list unpacking
(spread) to produce a cleaner expression: update the csv_columns construction
that currently uses ["Ressourcen ID", "Titel"] + FIELD_ORDER to use ["Ressourcen
ID", "Titel", *FIELD_ORDER]; keep the rest of the CSV writing logic (with
open(OUTPUT_FILE, ...), csv.DictWriter, writer.writeheader(),
writer.writerows(rows)) unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@examples/resource_extra_fields_export.py`:
- Around line 161-165: The code that converts list-type fields into a
comma-separated string should coerce each element to a string before joining to
avoid TypeError for non-string items; update the block that checks
isinstance(value, list) (using the variables value, field, row) to build a list
of strings (e.g., [str(v) for v in value]) and then join that list, so
row[field] gets a safe string representation.
- Around line 145-152: Wrap the json.loads call that parses item.metadata in a
try/except to catch json.JSONDecodeError (and optionally ValueError), so
malformed JSON doesn't crash the export; on error set extra_fields = {} (and/or
metadata = {}) and log or warn about the bad payload referencing item.metadata
or item.id for debugging. Specifically update the block that assigns metadata =
json.loads(metadata_raw) / extra_fields = metadata.get("extra_fields", {}) to
handle parsing failures and fall back to an empty dict for extra_fields.
- Around line 212-220: The loop over "for cell in col" silently swallows all
exceptions via a bare except; change it to catch specific exceptions (e.g.,
TypeError, ValueError, AttributeError) when calling len(str(cell.value)) and
handle them explicitly—either skip the cell or record/log the error—so
max_length is still computed safely; update the block around the "for cell in
col" loop and the calculation of adjusted_width and
ws.column_dimensions[column_letter].width to use the specific exception types
and emit a warning (or use the module logger) when an unexpected cell value is
encountered.
- Around line 36-47: The example hardcodes sensitive configuration
(API_HOST_URL, API_KEY) and other settings (CATEGORY_ID, OUTPUT_FILE,
OUTPUT_XLSX); replace those constants with reads from environment variables
(e.g., os.environ.get or dotenv) and add explicit validation/fallbacks—require
API_KEY and API_HOST_URL to be set and raise a clear error if missing, and allow
CATEGORY_ID, OUTPUT_FILE, OUTPUT_XLSX to default from env vars if present;
update any top-level comments to instruct users to set env vars rather than
paste keys into the file.

---

Nitpick comments:
In `@examples/resource_extra_fields_export.py`:
- Around line 121-131: The script runs top-level logic (prints and calls
itemsApi.read_items with CATEGORY_ID) which prevents reuse and testing; move the
execution into a main function (e.g., def main(): containing the print("Starting
export..."), items = itemsApi.read_items(cat=CATEGORY_ID), and print(f"Items
found: {len(items)}")) and then add the standard guard if __name__ ==
"__main__": main() so the module can be imported without side effects.
- Around line 173-182: Replace the list concatenation used to build csv_columns
with Python's list unpacking (spread) to produce a cleaner expression: update
the csv_columns construction that currently uses ["Ressourcen ID", "Titel"] +
FIELD_ORDER to use ["Ressourcen ID", "Titel", *FIELD_ORDER]; keep the rest of
the CSV writing logic (with open(OUTPUT_FILE, ...), csv.DictWriter,
writer.writeheader(), writer.writerows(rows)) unchanged.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a67caef and ee01749.

📒 Files selected for processing (1)
  • examples/resource_extra_fields_export.py

Comment on lines +145 to +152
# Metadata is stored as JSON string
metadata_raw = item.metadata

if metadata_raw:
metadata = json.loads(metadata_raw)
extra_fields = metadata.get("extra_fields", {})
else:
extra_fields = {}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

We must anticipate malformed data, Number One.

The json.loads call will raise a JSONDecodeError if the metadata contains invalid JSON. In space—and in data processing—we prepare for the unexpected.

🛡️ Proposed fix: Handle JSON parsing errors
     # Metadata is stored as JSON string
     metadata_raw = item.metadata

     if metadata_raw:
-        metadata = json.loads(metadata_raw)
-        extra_fields = metadata.get("extra_fields", {})
+        try:
+            metadata = json.loads(metadata_raw)
+            extra_fields = metadata.get("extra_fields", {})
+        except json.JSONDecodeError:
+            print(f"Warning: Invalid metadata JSON for item {item.id}")
+            extra_fields = {}
     else:
         extra_fields = {}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/resource_extra_fields_export.py` around lines 145 - 152, Wrap the
json.loads call that parses item.metadata in a try/except to catch
json.JSONDecodeError (and optionally ValueError), so malformed JSON doesn't
crash the export; on error set extra_fields = {} (and/or metadata = {}) and log
or warn about the bad payload referencing item.metadata or item.id for
debugging. Specifically update the block that assigns metadata =
json.loads(metadata_raw) / extra_fields = metadata.get("extra_fields", {}) to
handle parsing failures and fall back to an empty dict for extra_fields.

Comment on lines +161 to +165
# Convert list-type fields into comma-separated string
if isinstance(value, list):
value = ", ".join(value)

row[field] = value
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Ensure type safety before joining list values.

If value contains non-string elements, the join operation will fail. A minor adjustment ensures we navigate safely through any data anomalies.

🛡️ Proposed fix: Convert elements to strings
         # Convert list-type fields into comma-separated string
         if isinstance(value, list):
-            value = ", ".join(value)
+            value = ", ".join(str(v) for v in value)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/resource_extra_fields_export.py` around lines 161 - 165, The code
that converts list-type fields into a comma-separated string should coerce each
element to a string before joining to avoid TypeError for non-string items;
update the block that checks isinstance(value, list) (using the variables value,
field, row) to build a list of strings (e.g., [str(v) for v in value]) and then
join that list, so row[field] gets a safe string representation.

Comment on lines +212 to +220
for cell in col:
try:
if cell.value:
max_length = max(max_length, len(str(cell.value)))
except:
pass

adjusted_width = max_length + 2
ws.column_dimensions[column_letter].width = adjusted_width
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

A bare except clause is unworthy of Starfleet protocols.

This silently swallows all exceptions, making debugging nearly impossible when anomalies occur. We must be specific about what we catch, or at minimum, log the encounter.

🛡️ Proposed fix: Handle exceptions properly
     for cell in col:
         try:
             if cell.value:
                 max_length = max(max_length, len(str(cell.value)))
-        except:
-            pass
+        except (TypeError, AttributeError):
+            continue
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for cell in col:
try:
if cell.value:
max_length = max(max_length, len(str(cell.value)))
except:
pass
adjusted_width = max_length + 2
ws.column_dimensions[column_letter].width = adjusted_width
for cell in col:
try:
if cell.value:
max_length = max(max_length, len(str(cell.value)))
except (TypeError, AttributeError):
continue
adjusted_width = max_length + 2
ws.column_dimensions[column_letter].width = adjusted_width
🧰 Tools
🪛 Ruff (0.15.2)

[error] 216-216: Do not use bare except

(E722)


[error] 216-217: try-except-pass detected, consider logging the exception

(S110)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/resource_extra_fields_export.py` around lines 212 - 220, The loop
over "for cell in col" silently swallows all exceptions via a bare except;
change it to catch specific exceptions (e.g., TypeError, ValueError,
AttributeError) when calling len(str(cell.value)) and handle them
explicitly—either skip the cell or record/log the error—so max_length is still
computed safely; update the block around the "for cell in col" loop and the
calculation of adjusted_width and ws.column_dimensions[column_letter].width to
use the specific exception types and emit a warning (or use the module logger)
when an unexpected cell value is encountered.

Copy link
Collaborator

@MoustaphaCamara MoustaphaCamara left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: add to .gitignore:

diff --git a/.gitignore b/.gitignore                                                            
index 0ada496..31fb00d 100644                                                                   
--- a/.gitignore                                                                                
+++ b/.gitignore                                                                                
@@ -3,3 +3,4 @@ openapi.yaml                                                                    
 html                                                                                           
 venv                                                                                           
 __pycache__                                                                                    
+export.*                                                                                       
diff --git a/examples/resource_extra_fields_export.py b/examples/resource_extra_fields_export.py
index c91031b..0f8c553 100644                                                                   
--- a/examples/resource_extra_fields_export.py                                                  
+++ b/examples/resource_extra_fields_export.py                                                  
@@ -152,9 +152,12 @@ for item in items:                                                         
         extra_fields = {}                                                                      
                                                                                                
     # Extract all fields defined in FIELD_ORDER                                                
+    normalized_fields = {k.strip().lower(): v for k, v in extra_fields.items()}                
+                                                                                               
     for field in FIELD_ORDER:                                                                  
-        if field in extra_fields:                                                              
-            value = extra_fields[field].get("value", "")                                       
+        key = field.strip().lower()                                                            
+        if key in normalized_fields:                                                           
+            value = normalized_fields[key].get("value", "")                                    
         else:                                                                                  
             value = ""                                                                         

@MoustaphaCamara
Copy link
Collaborator

Thanks in advance for this addition! We'll make sure to merge it as soon as it's corrected :)

@MoustaphaCamara
Copy link
Collaborator

MoustaphaCamara commented Mar 6, 2026

I made a pull request in your fork so you can cherry pick my changes and keep what you need 👍
The script now works with previous versions (e.g., 5.0.2) and we just released the 5.4.1 which is compatible too. The 5.4.0 is erroned due to a fail in the API conception I introduced :p (fixed by Nico in elabftw/elabftw#6506 )

Co-authored-by: Mouss <104678778+MoustaphaCamara@users.noreply.github.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
examples/resource_extra_fields_export.py (2)

53-74: Number One, we appear to be reinventing the warp drive.

The examples/client.py module already provides a well-structured, reusable API client configuration that handles proxy settings, CA certificates, and debug flags. Duplicating this logic here creates maintenance burden and inconsistencies.

Additionally, the API key is configured twice: via configuration.api_key (lines 55-56) and via set_default_header (lines 68-71). The client.py pattern uses only set_default_header, which is the authoritative method for this library.

♻️ Proposed refactor: Reuse the existing client module
-import elabapi_python
 import json
 import csv
+import os
 from openpyxl import Workbook
 from openpyxl.styles import Font
 from openpyxl.utils import get_column_letter

+# Import the shared API client configuration
+from client import api_client
+import elabapi_python

 #########################
 #        CONFIG         
 #########################

-# Base URL of your eLabFTW API (must include /api/v2)
-API_HOST_URL = 'https://YOUR-URL/api/v2'
-
-# Personal API key generated in eLabFTW
-API_KEY = 'YOUR API Key'
-
 # Category ID from which items should be exported
-CATEGORY_ID = 123456
+CATEGORY_ID = int(os.environ.get('ELAB_CATEGORY_ID', '123456'))

 # Output filenames
-OUTPUT_FILE = "export.csv"
-OUTPUT_XLSX = "export.xlsx"
-
-#########################
-#   API CONFIGURATION   
-#########################
-
-# Create configuration object for the eLabFTW API
-configuration = elabapi_python.Configuration()
-configuration.api_key['api_key'] = API_KEY
-configuration.api_key_prefix['api_key'] = 'Authorization'
-configuration.host = API_HOST_URL
-
-configuration.debug = False
-
-# Set to True if valid SSL certificates are used
-configuration.verify_ssl = True
-
-# Create API client
-api_client = elabapi_python.ApiClient(configuration)
-
-# Set authorization header manually (required for authentication)
-api_client.set_default_header(
-    header_name='Authorization',
-    header_value=API_KEY
-)
+OUTPUT_FILE = os.environ.get('EXPORT_CSV', 'export.csv')
+OUTPUT_XLSX = os.environ.get('EXPORT_XLSX', 'export.xlsx')

 # Load Items API endpoint
 itemsApi = elabapi_python.ItemsApi(api_client)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/resource_extra_fields_export.py` around lines 53 - 74, The code
duplicates client setup and sets the API key twice; instead import and reuse the
shared client builder from examples.client (e.g., the factory function that
returns an ApiClient) rather than instantiating
elabapi_python.Configuration/ApiClient directly, remove the
configuration.api_key[...] assignment and keep only the authoritative
set_default_header usage, and then instantiate ItemsApi with the reused
ApiClient (look for symbols like Configuration, api_key, set_default_header,
ApiClient, ItemsApi and the shared factory in examples.client) so proxy/CA/debug
logic is centralized.

176-176: A minor course correction, as recommended by our static analysis sensors.

The linter suggests using spread syntax for list concatenation, which is more idiomatic in modern Python.

♻️ Proposed fix
-csv_columns = ["Ressourcen ID", "Titel"] + FIELD_ORDER
+csv_columns = ["Ressourcen ID", "Titel", *FIELD_ORDER]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/resource_extra_fields_export.py` at line 176, Replace the list
concatenation that builds csv_columns using the + operator with Python's list
unpacking: construct a new list literal containing the two initial strings
("Ressourcen ID" and "Titel") and expand FIELD_ORDER into it using the
star-unpacking operator; update the assignment to csv_columns to use this
pattern so csv_columns becomes a single list literal with the items from
FIELD_ORDER unpacked.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@examples/resource_extra_fields_export.py`:
- Around line 125-131: Wrap the script execution in a main guard (if __name__ ==
"__main__":) so importing this module won't run the export immediately, and add
defensive error handling around the itemsApi.read_items call: call
itemsApi.read_items(cat=CATEGORY_ID) inside a try/except that catches broad
request/API exceptions (and Exception as a fallback), logs/prints a clear
diagnostic including the exception message and returns/exit non‑zero on failure;
ensure variables referenced after the call (like items and len(items)) are only
used when the call succeeds. Reference: itemsApi.read_items and the top-level
script body where the print("Starting export...") and items =
itemsApi.read_items(...) occur.

---

Nitpick comments:
In `@examples/resource_extra_fields_export.py`:
- Around line 53-74: The code duplicates client setup and sets the API key
twice; instead import and reuse the shared client builder from examples.client
(e.g., the factory function that returns an ApiClient) rather than instantiating
elabapi_python.Configuration/ApiClient directly, remove the
configuration.api_key[...] assignment and keep only the authoritative
set_default_header usage, and then instantiate ItemsApi with the reused
ApiClient (look for symbols like Configuration, api_key, set_default_header,
ApiClient, ItemsApi and the shared factory in examples.client) so proxy/CA/debug
logic is centralized.
- Line 176: Replace the list concatenation that builds csv_columns using the +
operator with Python's list unpacking: construct a new list literal containing
the two initial strings ("Ressourcen ID" and "Titel") and expand FIELD_ORDER
into it using the star-unpacking operator; update the assignment to csv_columns
to use this pattern so csv_columns becomes a single list literal with the items
from FIELD_ORDER unpacked.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: db666e3f-4259-40b2-ae9a-9ef784de1937

📥 Commits

Reviewing files that changed from the base of the PR and between ee01749 and ce58347.

📒 Files selected for processing (1)
  • examples/resource_extra_fields_export.py

Comment on lines +125 to +131
print("Starting export...")

# Retrieve all items from the specified category
items = itemsApi.read_items(cat=CATEGORY_ID)

print(f"Items found: {len(items)}")

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Engage defensive protocols: the API call lacks error handling.

The read_items call on line 128 can fail due to network issues, authentication errors, or invalid category IDs. Currently, any exception would crash the script without a clear diagnostic message. Additionally, this script lacks a if __name__ == "__main__": guard, meaning it executes immediately upon import—problematic if someone attempts to reuse functions from this module.

🛡️ Proposed fix: Add main guard and API error handling
+def main():
+    print("Starting export...")
 
-print("Starting export...")
+    # Retrieve all items from the specified category
+    try:
+        items = itemsApi.read_items(cat=CATEGORY_ID)
+    except elabapi_python.ApiException as e:
+        print(f"API request failed: {e}")
+        return
 
-# Retrieve all items from the specified category
-items = itemsApi.read_items(cat=CATEGORY_ID)
+    print(f"Items found: {len(items)}")
+    # ... rest of logic ...
 
-print(f"Items found: {len(items)}")
+if __name__ == "__main__":
+    main()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/resource_extra_fields_export.py` around lines 125 - 131, Wrap the
script execution in a main guard (if __name__ == "__main__":) so importing this
module won't run the export immediately, and add defensive error handling around
the itemsApi.read_items call: call itemsApi.read_items(cat=CATEGORY_ID) inside a
try/except that catches broad request/API exceptions (and Exception as a
fallback), logs/prints a clear diagnostic including the exception message and
returns/exit non‑zero on failure; ensure variables referenced after the call
(like items and len(items)) are only used when the call succeeds. Reference:
itemsApi.read_items and the top-level script body where the print("Starting
export...") and items = itemsApi.read_items(...) occur.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants